Add live-debugger plugin#296
Conversation
221a99f to
0704915
Compare
This stack of pull requests is managed by Graphite. Learn more about stacking. |
6614e50 to
f9499d8
Compare
e9873f5 to
2f6900f
Compare
yoannmoinet
left a comment
There was a problem hiding this comment.
Pretty solid PR, well done!
I have some concerns though.
A ~1.2x build overhead is not "negligible", that's 20%.
Totally not a blocker, but I don't want to misrepresent the impact of this.
I'm not a fan of adding babel for everyone, especially with Unplugin shipping with rollup's this.parse for both rollup and vite (and rolldown), which makes babel redundant in these cases.
It could also be set as peer dependencies or optional dependencies, and verified at runtime. So not everyone installing build-plugins has to pay the price of such dependencies.
For bundlers other than Rollup, Rolldown, or Vite,
setParseImplmust be called to manually provide a parser implementation. Parsers such as Acorn, Babel, or Oxc can be used.
I'd prefer you to follow this way of adding a parser to the ecosystem, could sit in a different, independent PR too, it would lighten this one.
Note that we have a parallel effort in that regard, led by @sdkennedy2, and they want to use oxc, which is more modern than babel imo, and a better option. For instance, oxc would prevent some workarounds you have to do with babel.
Sorry, I was referring to the runtime overhead being negligible. But I can see the PR body could easily be misunderstood. I've re-phrased it. |
yoannmoinet
left a comment
There was a problem hiding this comment.
Just a nit on the pattern we use for enable in other plugins.
Other than that, lgtm.
Especially if it's not "public" yet, and that it uses peer/optional dependencies for @babel/*.
I won't be as strict. Outside's impact is pretty minimal.
3715b65 to
6e4d5aa
Compare
| // Validate enable option | ||
| if (pluginConfig.enable !== undefined && typeof pluginConfig.enable !== 'boolean') { | ||
| errors.push(`${red('enable')} must be a boolean`); | ||
| } |
There was a problem hiding this comment.
A bit overkill but that's fine haha.
Introduce a live-debugger plugin that instruments JavaScript functions at build time for Datadog's Live Debugger product. The plugin uses Babel to inject probe declarations and snapshot helpers into user code, enabling runtime debugging without redeploying. Key features: - AST-based function instrumentation via Babel transform - Scope tracking for variable capture at probe locations - Deterministic function ID generation for probe targeting - Configurable file include/exclude filters - File and function count limits to bound overhead - CJS/ESM interop for Babel dependencies in bundled environments Includes unit tests, integration tests, E2E tests across all supported bundlers, and a benchmark script for performance validation.
The bump commit updated yarn.lock only; `yarn cli integrity` also needs to remove the stale 1.5.0 cache zip and refresh LICENSES-3rdparty.csv with the new attribution for @jridgewell/sourcemap-codec and @jridgewell/gen-mapping, and drop @babel/helper-globals (no longer pulled in by any resolved @babel/traverse version).
The loader.ts indirection did `require('./index')` from a file that
Rollup bundles together with `./index` itself. In the published bundle,
`./index` resolves to the bundled entry — a circular require that
returned the plugin's public exports instead of the transform module.
`getTransformCode()` then cached `undefined` for `transformCode`,
instrumentation silently failed at every transform hook, and the E2E
test checking that `$dd_entry` fires (active-probes test) saw 0.
Achieve the same lazy-loading guarantee without the bundler landmine:
- Convert `import * as t from '@babel/types'` to `import type` in
`functionId.ts`, `scopeTracker.ts`, and `instrumentation.ts`, and
thread a `typesModule: typeof import('@babel/types')` parameter
through the helpers that need runtime type guards (same pattern
`collectReturnStatements`/`alwaysReturns` already used).
- Move `requireOptionalPeerDep` + the diagnostic-wrapping helpers
into `transform/index.ts` so they're co-located with their only
caller and don't need a separate module.
- Delete `transform/loader.ts`; `src/index.ts` goes back to a direct
`import { transformCode } from './transform'`.
Net effect: no peer dep is loaded until a file actually reaches the
transform hook, the four `require("@babel/*"|"magic-string")` calls
survive Rollup as externalized literal requires (verified in all 5
published bundles), and the missing-peer-dep error message is
unchanged.
Also extends `lazy-deps.test.ts` to cover every peer dep via
`it.each(PEER_DEPS)` and asserts the install-hint is present in the
rewrapped error.
Align `@dd/live-debugger-plugin` with the convention used by the `rum`,
`error-tracking`, and `output` plugins: enablement is derived from the
presence of the `liveDebugger` config key, not from a dedicated `enable`
sub-property. Passing `liveDebugger: {}` now enables the plugin with
default options; omitting the key keeps it disabled.
- validate.ts: use `enable: !!config[CONFIG_KEY]`, ignoring the `enable`
sub-property like the other plugins do.
- README: drop the `liveDebugger.enable` section and its TOC entry, and
remove the now-redundant `enable: true` from example snippets.
- transform/index.ts: update the missing-peer-dep error message to say
"when the `liveDebugger` plugin is enabled" instead of referencing
`liveDebugger.enable`.
- Tests: rewrite the defaults and `getPlugins` cases to reflect the new
semantics (undefined -> disabled, `{}` -> enabled).
The previous commit reduced enablement to `!!config[CONFIG_KEY]`, which
meant `{ liveDebugger: { enable: false } }` silently kept the plugin
enabled. Every other plugin in this repo (`apps`, `error-tracking`,
`metrics`, `output`, `rum`) accepts an explicit `enable: false` — either
directly (`apps`) or implicitly via spread ordering (the rest).
Switch `validate.ts` to the explicit `apps`-style pattern:
enable: pluginConfig.enable ?? !!config[CONFIG_KEY]
so that:
- omitted `liveDebugger` key -> disabled
- `liveDebugger: {}` -> enabled
- `liveDebugger: { enable: true }` -> enabled
- `liveDebugger: { enable: false }` -> disabled
Also add a runtime `typeof enable === 'boolean'` check alongside the
existing boolean-option validations, and re-document `liveDebugger.enable`
in the README to match the `apps` README wording.
Test coverage:
- Restore the "return an empty array when enable is false" test in
`index.test.ts`.
- Add `enable: false` / `enable: true` cases to `validateOptions` defaults
tests and a new `invalid enable` describe block covering non-boolean
inputs. Extend the `multiple errors` aggregate test to include `enable`.
81c7bfd to
795ac49
Compare

Supersedes #253
What and why?
Adds a new Live Debugger build plugin (
@dd/live-debugger-plugin) that automatically instruments JavaScript/TypeScript functions at build time to enable Live Debugger without requiring code rebuilds.When enabled, every matching function in the application is wrapped with lightweight probes that can be activated at runtime via
$dd_probes(functionId). When no probes are active the check returnsundefinedand all instrumentation is skipped.This originated as an Innovation Week POC and has been hardened with bug fixes, performance optimizations (down from ~17x to ~1.2x build overhead), comprehensive test coverage, and filtering options.
Notes:
<relative-file-path>;<function-name>) is a placeholder — the final stable ID scheme will be implemented in a follow-up PR before this is ready for end user consumption.Overhead stats
Installation
Babel (
@babel/parser,@babel/traverse,@babel/types) andmagic-stringare declared as optional peer dependencies on the 5 published bundler packages. Users who don't enable Live Debugger don't pay the install-size cost (~12 MB of Babel +magic-string). When the plugin is enabled (by providing aliveDebuggerconfiguration), projects install the peer deps alongside the Datadog plugin:If any of them is missing at build time, the plugin throws with an actionable install hint instead of a raw Node
MODULE_NOT_FOUND.How?
New plugin (
packages/plugins/live-debugger/)src/transform/): Uses Babel to parse the AST in read-only mode, collects instrumentation targets, then applies injections via MagicString (no AST mutation). Processes inner functions before outer ones so thatappendLeftcalls at shared positions stack correctly.src/transform/loader.ts): Gates the entire transform module behindgetTransformCode(), so Babel andmagic-stringare not loaded at all when theliveDebuggerconfiguration is omitted. When the plugin is enabled, peer deps are loaded on the first file that reaches the transform hook, and a missing module is rewrapped with a clear install hint.src/transform/functionId.ts): Produces human-readable IDs in<relative-file-path>;<function-name>format. Anonymous functions use<anonymous>@line:col:siblingIndexfor disambiguation. This algorithm is temporary and will be replaced with a stable, production-grade scheme in a follow-up.src/transform/scopeTracker.ts): Extracts variable names (params + locals, capped at 25) for entry and exit snapshots so probes can capture local state.src/transform/instrumentation.ts): Skips functions that can't be safely instrumented (generators, async generators, class constructors) and respects// @dd-no-instrumentationskip comments.src/transform/cjs-interop.ts): Handles@babel/traverseCJS default-export resolution in bundled environments where the module wrapper shape can vary.src/validate.ts): Validates all user-supplied config with descriptive error messages.Configuration options (providing a
liveDebuggerobject — even an empty{}— enables the plugin, matching the convention used by the other product plugins):include/exclude— file patterns to control scope (defaults include.js/.jsx/.ts/.tsx, excludenode_modules, minified files, virtual modules, Datadog SDK packages, etc.)honorSkipComments— respect// @dd-no-instrumentationcomments (default:true)functionTypes— restrict to specific function kinds (e.g.,functionDeclaration,arrowFunction,classMethod)namedOnly— skip anonymous functions (default:false)The env variable
DD_LD_LIMITcan cap the number of files with functions that are processed, as a safety valve during dogfooding.Factory integration:
packages/factory/src/index.tsandpackages/core/src/types.tsvia the standard plugin injection markers.magic-stringdeclared as optionalpeerDependenciesso they're only installed by consumers who enable Live Debugger.hideFromRootReadme: trueuntil the plugin is production-ready.Runtime stubs:
context.inject(). This definesglobalThis.$dd_probesas an empty function so instrumented code never crashes when the Datadog Browser Debugger SDK (@datadog/browser-debugger) is absent.$dd_entry,$dd_return, and$dd_throwdon't need stubs because they are always guarded byif (probe)checks in the injected instrumentation.DD_DEBUGGER.init()is called, it overwrites$dd_probeswith the real implementation and sets up the other three globals. Probes activate immediately — no rebuild required.Testing & benchmarks:
DD_LD_LIMITcapping, error handling, and option validationscripts/benchmark-subset.js) for measuring build overhead on real file subsets